﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml;
using System.Text.RegularExpressions;

namespace Nebula2XML
{
	enum XMLElement
	{
		NebulaList,
		Nebula,
		Number,
		Description,
	}

	enum XMLAttribute
	{
		RightAscension,
		Declination,
		Magnitude,
		SurfaceBrightness,
		Class,
		Type,
		Catalog,
	}

	enum NumberType
	{
		Unknown,
		NGC,
		IC,
		Melotte,
		Messier,
		UGC,
		OCL,
	}

	enum NebulaType
	{
		Asterism,
		BrightNebula, //Bright Nebula
		ClusterNebulosity, //Cluster with Nebulosity
		DarkNebula, //Dark Nebula
		GalaxyCluster, //Galaxy cluster
		Galaxy, //Galaxy
		GlobularCluster, //Globular Cluster
		GalaxyDiffuseNebula, //Diffuse Nebula in a Galaxy
		GalaxyGlobularCluster, //Globular Cluster in a Galaxy
		GalaxyClusterNebulosity, //Cluster with Nebulosity in a Galaxy
		LMCClusterNebulosity, //Cluster with Nebulosity in the LMC
		LMCDiffuseNebula, //Diffuse Nebula in the LMC
		LMCGlobularCluster, //Globular Cluster in the LMC
		LMCOpenCluster, //Open cluster in the LMC
		Nonexistent,
		OpenCluster, //Open Cluster
		PlanetaryNebula, //Planetary Nebula
		SMCClusterNebulosity, //Cluster with Nebulosity in the SMC
		SMCDiffuseNebula, //Diffuse Nebula in the SMC
		SMCGlobularCluster, //Globular Cluster in the SMC
		SMCOpenCluster, //Open cluster in the SMC
		SupernovaRemnant, //Supernova Remnant
		Quasar,
		Stars //# Stars
	}

	[Flags]
	enum BCHMType
	{
		NGC = 1, //Best of the NGC from SAC
		Caldwell = 2, //Caldwell catalog from Patrick Moore
		Herschel400 = 4, //Herschel 400 from Astronomical League
		Messier = 8, //Messier object
	}

	internal static class ReaderExtensions
	{
		static string[] TypeCode = new string[]
		{
			"ASTER", //Asterism
			"BRTNB", //Bright Nebula
			"CL+NB", //Cluster with Nebulosity
			"DRKNB", //Dark Nebula
			"GALCL", //Galaxy cluster
			"GALXY", //Galaxy
			"GLOCL", //Globular Cluster
			"GX+DN", //Diffuse Nebula in a Galaxy
			"GX+GC", //Globular Cluster in a Galaxy
			"G+C+N", //Cluster with Nebulosity in a Galaxy
			"LMCCN", //Cluster with Nebulosity in the LMC
			"LMCDN", //Diffuse Nebula in the LMC
			"LMCGC", //Globular Cluster in the LMC
			"LMCOC", //Open cluster in the LMC
			"NONEX", //Nonexistent
			"OPNCL", //Open Cluster
			"PLNNB", //Planetary Nebula
			"SMCCN", //Cluster with Nebulosity in the SMC
			"SMCDN", //Diffuse Nebula in the SMC
			"SMCGC", //Globular Cluster in the SMC
			"SMCOC", //Open cluster in the SMC
			"SNREM", //Supernova Remnant
			"QUASR", //Quasar
		};

		static char[] BCHMChar = new char[]
		{
			'B', //Best of the NGC from SAC
			'C', //Caldwell catalog from Patrick Moore
			'H', //Herschel 400 from Astronomical League
			'M', //Messier object
		};

		internal static void ForEachData(this BinaryReader reader, Action<int> body)
		{
			int count = reader.ReadInt32();
			for (int i = 0; i < count; i++)
				body(i);
		}

		internal static void ReadNumber(this BinaryReader reader, out string number, out NumberType type)
		{
			number = reader.ReadString();
			type = NumberType.Unknown;

			if (Regex.IsMatch(number, @"NGC\s[\s\d]{4}"))
			{
				type = NumberType.NGC; //New General Catalog of Nebulae & Clusters of Stars
				number = number.Substring(4).Trim();
			}
			else if (Regex.IsMatch(number, @"IC\s\s[\s\d]{4}"))
			{
				type = NumberType.IC; //1st and 2nd Index Catalogs to the NGC
				number = number.Substring(4).Trim();
			}
			else if (Regex.IsMatch(number, @"Mel\s[\s\d]{3}"))
			{
				type = NumberType.Melotte; //Melotte (open clusters)
				number = number.Substring(4).Trim();
			}
			else if (Regex.IsMatch(number, @"M\d+"))
			{
				type = NumberType.Messier; //Messier objects
				number = number.Substring(1);
			}
			else if (Regex.IsMatch(number, @"UGC\s[\s\d]{5}"))
			{
				type = NumberType.UGC; //Uppsala General Catalog (galaxies)
				number = number.Substring(4).Trim();
			}
			else if (Regex.IsMatch(number, @"OCL\s[\s\d]{4}"))
			{
				type = NumberType.OCL;
				number = number.Substring(4).Trim();
			}
		}

		internal static NebulaType ReadType(this BinaryReader reader)
		{
			string code = reader.ReadString();
			int index = Array.IndexOf(TypeCode, code);
			if (index >= 0)
				return (NebulaType)index;
			else if (Regex.IsMatch(code, @"\dSTAR"))
				return NebulaType.Stars + int.Parse(code.Substring(0, 1));
			else
				throw new IndexOutOfRangeException("Type");
		}

		internal static BCHMType ReadBCHM(this BinaryReader reader)
		{
			string code = reader.ReadString();
			BCHMType BCHM = 0;
			Array values = Enum.GetValues(typeof(BCHMType));
			foreach (char c in code.ToCharArray())
			{
				int index = Array.IndexOf(BCHMChar, c);
				if (index < 0)
					throw new IndexOutOfRangeException("BCHM");
				BCHM |= (BCHMType)values.GetValue(index);
			}
			return BCHM;
		}
	}

	internal static class WriterExtensions
	{
		const string NS = "urn:denebsoft-skymap-nebula";

		internal static void WriteDocumentElement(this XmlWriter writer, XMLElement name, Action body)
		{
			writer.WriteStartDocument();
			writer.WriteStartElement(name.ToString(), NS);
			body();
			writer.WriteEndElement();
			writer.WriteEndDocument();
		}

		internal static void WriteElement(this XmlWriter writer, XMLElement name, Action body)
		{
			writer.WriteStartElement(name.ToString(), NS);
			body();
			writer.WriteEndElement();
		}

		internal static void WriteElement(this XmlWriter writer, XMLElement name, string value, Action body)
		{
			if (value == " ") return;
			writer.WriteStartElement(name.ToString(), NS);
			body();
			writer.WriteString(value);
			writer.WriteEndElement();
		}

		internal static void WriteAttribute(this XmlWriter writer, XMLAttribute name, double value)
		{
			writer.WriteAttributeString(name.ToString(), XmlConvert.ToString(value));
		}

		internal static void WriteAttribute(this XmlWriter writer, XMLAttribute name, string value)
		{
			if (value != " ")
				writer.WriteAttributeString(name.ToString(), value);
		}

		internal static void WriteAttribute(this XmlWriter writer, XMLAttribute name, NebulaType value)
		{
			if (value < NebulaType.Stars)
				writer.WriteAttributeString(name.ToString(), value.ToString());
			else
				writer.WriteAttributeString(name.ToString(),
					((int)(value - NebulaType.Stars)).ToString() + NebulaType.Stars.ToString());
		}

		internal static void WriteAttribute<TEnum>(this XmlWriter writer, XMLAttribute name, TEnum value, TEnum defValue) where TEnum: struct
		{
			if (!value.Equals(defValue))
				writer.WriteAttributeString(name.ToString(), value.ToString().Replace(", ", " "));
		}

		internal static void WriteAttributeLanguage(this XmlWriter writer, string lang)
		{
			writer.WriteAttributeString("xml", "lang", null, lang);
		}

		internal static void Write(this XmlWriter writer, string value)
		{
			writer.WriteString(value);
		}
	}

	class Program
	{
		static void Main(string[] args)
		{
			string ifn = args.Length == 0 || Path.GetExtension(args[0]) == ".xml" ? "nebula.dat" : args[0];
			string ofn = args.Length == 1 && Path.GetExtension(args[0]) == ".xml" ? args[0] : args.Length <= 1 ? "nebula.xml" : args[1];

			if (args.Length > 2 || Path.GetExtension(ifn) != ".dat" || Path.GetExtension(ofn) != ".xml" ||
				args.Length > 1 && Path.GetExtension(args[0]) == ".xml")
			{
				Console.WriteLine("usage: {0} [nebula.dat] [nebula.xml]", Path.GetFileNameWithoutExtension(System.Reflection.Assembly.GetExecutingAssembly().Location));
				return;
			}

			using (var ifs = new FileStream(ifn, FileMode.Open))
			using (var reader = new BinaryReader(ifs))
			using (var writer = XmlWriter.Create(ofn, new XmlWriterSettings()
			{
				Encoding = Encoding.UTF8,
				Indent = true,
				IndentChars = "\t",
				OmitXmlDeclaration = false
			}))
			{
				writer.WriteDocumentElement(XMLElement.NebulaList, () =>
				{
					reader.ForEachData(i =>
					{
						writer.WriteElement(XMLElement.Nebula, () =>
						{
							string Name; NumberType NameType;
							reader.ReadNumber(out Name, out NameType);
							string OtherName; NumberType OtherType;
							reader.ReadNumber(out OtherName, out OtherType);
							double RA = reader.ReadDouble();
							double Dec = reader.ReadDouble();
							double magnitude = reader.ReadDouble();
							double surfaceBr = reader.ReadDouble();
							string Class = reader.ReadString();
							NebulaType Type = reader.ReadType();
							var BCHM = reader.ReadBCHM();
							string Description = reader.ReadString();
							string Notes = reader.ReadString();

							writer.WriteAttribute(XMLAttribute.RightAscension, RA); //Right Ascension
							writer.WriteAttribute(XMLAttribute.Declination, Dec);
							writer.WriteAttribute(XMLAttribute.Magnitude, magnitude);
							writer.WriteAttribute(XMLAttribute.SurfaceBrightness, surfaceBr); //Surface brightness (Reference Catalog of Galaxies 3)
							writer.WriteAttribute(XMLAttribute.Class, Class);
							writer.WriteAttribute(XMLAttribute.Type, Type);
							writer.WriteAttribute(XMLAttribute.Catalog, BCHM, (BCHMType)0);

							writer.WriteElement(XMLElement.Number, Name, () =>
							{
								writer.WriteAttribute(XMLAttribute.Type, NameType, NumberType.Unknown);
							});

							writer.WriteElement(XMLElement.Number, OtherName, () =>
							{
								writer.WriteAttribute(XMLAttribute.Type, OtherType, NumberType.Unknown);
							});

							writer.WriteElement(XMLElement.Description, Description, () =>
							{
								writer.WriteAttributeLanguage("en");
								writer.WriteAttribute(XMLAttribute.Type, NumberType.NGC, NumberType.Unknown);
							});

							writer.WriteElement(XMLElement.Description, Notes, () =>
							{
								writer.WriteAttributeLanguage("en");
							});
						});
					});
				});
			}
		}
	}
}
